/*
 *  This file is part of the Image Metadata Editor.
 *
 *  Copyright (C) 2015 Australian Institute of Marine Science
 *
 *  Contact: Gael Lafond <g.lafond@aims.org.au>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package au.gov.aims.exif.editor;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

public enum MagicNumber {
	// http://en.wikipedia.org/wiki/Magic_number_(programming)
	// http://stackoverflow.com/questions/1915317/howto-extract-mimetype-from-a-byte
	MIDI("MIDI", new byte[][] { { (byte)0x4d, (byte)0x54, (byte)0x68, (byte)0x64 } }),
	PS("PS",     new byte[][] { { (byte)0x25, (byte)0x21 } }),
	PDF("PDF",   new byte[][] { { (byte)0x25, (byte)0x50, (byte)0x44, (byte)0x46 } }),
	JPG("JPG",   new byte[][] { { (byte)0xff, (byte)0xd8 } }),
	RAR("RAR",   new byte[][] { { (byte)0x52, (byte)0x61, (byte)0x72, (byte)0x21 } }),
	GIF("GIF",   new byte[][] { { (byte)0x47, (byte)0x49, (byte)0x46, (byte)0x38, (byte)0x37, (byte)0x61 },    // GIF87a
	                            { (byte)0x47, (byte)0x49, (byte)0x46, (byte)0x38, (byte)0x39, (byte)0x61 } }), // GIF89a
	PNG("PNG",   new byte[][] { { (byte)0x89, (byte)0x50, (byte)0x4e, (byte)0x47, (byte)0x0d, (byte)0x0a, (byte)0x1a, (byte)0x0a } }),
	ZIP("ZIP",   new byte[][] { { (byte)0x50, (byte)0x4b } }),
	TIFF("TIFF", new byte[][] { { (byte)0x49, (byte)0x49, (byte)0x2a, (byte)0x00 },    // Little endian TIFF
	                            { (byte)0x4d, (byte)0x4d, (byte)0x00, (byte)0x2a } }), // Big endian TIFF
	BMP("BMP",   new byte[][] { { (byte)0x42, (byte)0x4d } }), // "BM"
	MSO("MSO",   new byte[][] { { (byte)0xd0, (byte)0xcf, (byte)0x11, (byte)0xe0 } }); // Microsoft Office document

	private static final Logger LOGGER = Logger.getLogger(MagicNumber.class.getName());

	private final String type;
	private final byte[][] bytes;
	// The maximum number of bytes needed to identify a magic number. This is used to know how many bytes are needed to be extracted from the file.
	public static int maxBytes = -1;

	private MagicNumber(String type, byte[][] bytes) {
		this.type = type;
		this.bytes = bytes;
	}

	public String getType() {
		return this.type;
	}
	public byte[][] getBytes() {
		return this.bytes;
	}

	public static MagicNumber getMagicNumber(File file) throws IOException {
		return getMagicNumber(getMagicNumberBytes(file));
	}

	public static MagicNumber getMagicNumber(InputStream is) throws IOException {
		return getMagicNumber(getMagicNumberBytes(is));
	}

	public static MagicNumber getMagicNumber(byte[] fileMagicNumberBytes) {
		for (MagicNumber magicNumber : MagicNumber.values()) {
			if (match(fileMagicNumberBytes, magicNumber)) {
				return magicNumber;
			}
		}
		return null;
	}

	/**
	 * Used for debugging
	 * @param file The file from which we want to get the magic number.
	 * @return String representing the file magic number in hexadecimal.
	 */
	public static String getPrintableMagicNumber(File file) {
		if (file == null) {
			LOGGER.log(Level.WARNING, "File is null");
			return null;
		}
		if (!file.exists()) {
			LOGGER.log(Level.WARNING, "The file do not exists: " + file.getAbsolutePath());
			return null;
		}
		if (!file.canRead()) {
			LOGGER.log(Level.WARNING, "The file is not readable: " + file.getAbsolutePath());
			return null;
		}

		StringBuilder sb = new StringBuilder();

		byte[] fileMagicNumberBytes = null;
		try {
			fileMagicNumberBytes = getMagicNumberBytes(file);
		} catch(Exception ex) {
			LOGGER.log(Level.WARNING, "Can not get the magic number for the file: " + file.getAbsolutePath());
		}
		if (fileMagicNumberBytes != null) {
			for (byte b : fileMagicNumberBytes) {
				sb.append(String.format("%02X ", b));
			}
		}
		return sb.toString();
	}

	public static boolean match(byte[] fileMagicNumberBytes, MagicNumber magicNumber) {
		for (byte[] magicNumberBytes : magicNumber.bytes) {
			boolean matches = true;
			// Check if the magic number extracted from the file is long enough.
			// If it doesn't have as much bytes as in the magic number, it doesn't match.
			if (fileMagicNumberBytes == null || fileMagicNumberBytes.length < magicNumberBytes.length) {
				matches = false;
			} else {
				for (int i=0; matches && i<magicNumberBytes.length; i++) {
					if (fileMagicNumberBytes[i] != magicNumberBytes[i]) {
						matches = false;
					}
				}
			}
			if (matches) {
				return true;
			}
		}
		return false;
	}

	private static int getMaxBytes() {
		if (maxBytes < 0) {
			for (MagicNumber magicNumber : MagicNumber.values()) {
				for (byte[] magicNumberBytes : magicNumber.bytes) {
					if (magicNumberBytes.length > maxBytes) {
						maxBytes = magicNumberBytes.length;
					}
				}
			}
		}
		return maxBytes;
	}

	public static byte[] getMagicNumberBytes(File file) throws IOException {
		FileInputStream is = null;
		try {
			is = new FileInputStream(file);
			return getMagicNumberBytes(is);
		} finally {
			if (is != null) {
				try {
					is.close();
				} catch(Exception ex) {
					LOGGER.log(Level.WARNING, "Can not close the file input stream after attempting to extract its magic number", ex);
				}
			}
		}
	}

	public static byte[] getMagicNumberBytes(InputStream is) throws IOException {
		byte[] readMagicNumber = new byte[getMaxBytes()];
		int nbByte = is.read(readMagicNumber, 0, readMagicNumber.length);

		if (nbByte <= 0) {
			return null;
		}

		// Reformat the array (if less than "length" bytes has been read, the extra bytes has to be "removed" to avoid miss interpretations)
		byte[] magicNumber = new byte[nbByte];
		System.arraycopy(readMagicNumber, 0, magicNumber, 0, nbByte);

		return magicNumber;
	}
}
